/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.Collections;
using System.Globalization;
using Microsoft.Extensions.Logging;
using Spring.Collections;
using Spring.Core;
using Spring.Util;

namespace Spring.Objects.Factory.Config;

/// <summary>
/// Resolves placeholder values in one or more object definitions
/// </summary>
/// <remarks>
/// The placeholder syntax follows the NAnt style: <c>${...}</c>.
/// Placeholders values are resolved against a list of
/// <see cref="IVariableSource"/>s.  In case of multiple definitions
/// for the same property placeholder name, the first one in the
/// list is used.
/// <para>Variable substitution is performed on simple property values,
/// lists, dictionaries, sets, constructor
/// values, object type name, and object names in
/// runtime object references (
/// <see cref="Spring.Objects.Factory.Config.RuntimeObjectReference"/>).</para>
/// <para>Furthermore, placeholder values can also cross-reference other
/// placeholders, in the manner of the following example where the
/// <c>rootPath</c> property is cross-referenced by the <c>subPath</c>
/// property.
/// </para>
/// <example>
/// <code escaped="true">
/// <name-values>
///		<add key="rootPath" value="myrootdir"/>
///		<add key="subPath" value="${rootPath}/subdir"/>
/// </name-values>
/// </code>
/// </example>
/// <para>If a configurer cannot resolve a placeholder, and the value of the
/// <see cref="Spring.Objects.Factory.Config.PropertyPlaceholderConfigurer.IgnoreUnresolvablePlaceholders"/>
/// property is currently set to <see langword="false"/>, an
/// <see cref="Spring.Objects.Factory.ObjectDefinitionStoreException"/>
/// will be thrown. </para>
/// </remarks>
/// <author>Mark Pollack</author>
public class VariablePlaceholderConfigurer : IObjectFactoryPostProcessor, IPriorityOrdered
{
    /// <summary>
    /// The default placeholder prefix.
    /// </summary>
    public static readonly string DefaultPlaceholderPrefix = "${";

    /// <summary>
    /// The default placeholder suffix.
    /// </summary>
    public static readonly string DefaultPlaceholderSuffix = "}";

    private int order = Int32.MaxValue; // default: same as non-Ordered

    private bool includeAncestors;
    private bool ignoreUnresolvablePlaceholders;
    private string placeholderPrefix = DefaultPlaceholderPrefix;
    private string placeholderSuffix = DefaultPlaceholderSuffix;

    private IList variableSourceList = new ArrayList();

    /// <summary>
    /// Create a new instance without any variable sources
    /// </summary>
    public VariablePlaceholderConfigurer()
    {
    }

    /// <summary>
    /// Create a new instance and initialize with the given variable source
    /// </summary>
    /// <param name="variableSource"></param>
    public VariablePlaceholderConfigurer(IVariableSource variableSource)
    {
        this.VariableSource = variableSource;
    }

    /// <summary>
    /// Create a new instance and initialize with the given list of variable sources
    /// </summary>
    public VariablePlaceholderConfigurer(IList variableSources)
    {
        this.VariableSources = variableSources;
    }

    /// <summary>
    /// Sets the list of <see cref="IVariableSource"/>s that will be used to resolve placeholder names.
    /// </summary>
    /// <value>A list of <see cref="IVariableSource"/>s.</value>
    public IList VariableSources
    {
        set { variableSourceList = value; }
    }

    /// <summary>
    /// Sets <see cref="IVariableSource"/> that will be used to resolve placeholder names.
    /// </summary>
    /// <value>A <see cref="IVariableSource"/> instance.</value>
    public IVariableSource VariableSource
    {
        set
        {
            variableSourceList = new ArrayList();
            variableSourceList.Add(value);
        }
    }

    /// <summary>
    /// The placeholder prefix (the default is <c>${</c>).
    /// </summary>
    /// <seealso cref="DefaultPlaceholderPrefix"/>
    public string PlaceholderPrefix
    {
        set { placeholderPrefix = value; }
    }

    /// <summary>
    /// The placeholder suffix (the default is <c>}</c>)
    /// </summary>
    /// <seealso cref="DefaultPlaceholderSuffix"/>
    public string PlaceholderSuffix
    {
        set { placeholderSuffix = value; }
    }

    /// <summary>
    /// Indicates whether unresolved placeholders should be ignored.
    /// </summary>
    public bool IgnoreUnresolvablePlaceholders
    {
        set { ignoreUnresolvablePlaceholders = value; }
    }

    public bool IncludeAncestors
    {
        set { includeAncestors = value; }
    }

    /// <summary>
    /// Modify the application context's internal object factory after its
    /// standard initialization.
    /// </summary>
    /// <param name="factory">The object factory used by the application context.</param>
    /// <remarks>
    /// <p>
    /// All object definitions will have been loaded, but no objects will have
    /// been instantiated yet. This allows for overriding or adding properties
    /// even to eager-initializing objects.
    /// </p>
    /// </remarks>
    /// <exception cref="Spring.Objects.ObjectsException">
    /// In case of errors.
    /// </exception>
    public void PostProcessObjectFactory(IConfigurableListableObjectFactory factory)
    {
        if (CollectionUtils.IsEmpty(variableSourceList))
        {
            throw new ArgumentException("No VariableSources configured");
        }

        ICollection filtered = CollectionUtils.FindValuesOfType(this.variableSourceList, typeof(IVariableSource));
        if (filtered.Count != this.variableSourceList.Count)
        {
            throw new ArgumentException("'VariableSources' must contain IVariableSource elements only", "VariableSources");
        }

        try
        {
            ProcessProperties(factory);
        }
        catch (Exception ex)
        {
            if (typeof(ObjectsException).IsInstanceOfType(ex))
            {
                throw;
            }
            else
            {
                throw new ObjectsException(
                    "Errored while postprocessing an object factory.", ex);
            }
        }
    }

    /// <summary>
    /// Return the order value of this object, where a higher value means greater in
    /// terms of sorting.
    /// </summary>
    /// <returns>The order value.</returns>
    /// <seealso cref="Spring.Core.IOrdered.Order"/>
    public int Order
    {
        get { return order; }
        set { order = value; }
    }

    /// <summary>
    /// Apply the property replacement using the specified <see cref="IVariableSource"/>s for all
    /// object in the supplied
    /// <see cref="Spring.Objects.Factory.Config.IConfigurableListableObjectFactory"/>.
    /// </summary>
    /// <param name="factory">
    /// The <see cref="Spring.Objects.Factory.Config.IConfigurableListableObjectFactory"/>
    /// used by the application context.
    /// </param>
    /// <exception cref="Spring.Objects.ObjectsException">
    /// If an error occured.
    /// </exception>
    protected virtual void ProcessProperties(IConfigurableListableObjectFactory factory)
    {
        CompositeVariableSource compositeVariableSource = new CompositeVariableSource(variableSourceList);
        TextProcessor tp = new TextProcessor(this, compositeVariableSource);
        ObjectDefinitionVisitor visitor = new ObjectDefinitionVisitor(new ObjectDefinitionVisitor.ResolveHandler(tp.ParseAndResolveVariables));

        var objectDefinitionNames = factory.GetObjectDefinitionNames(includeAncestors);
        for (int i = 0; i < objectDefinitionNames.Count; ++i)
        {
            string name = objectDefinitionNames[i];
            IObjectDefinition definition = factory.GetObjectDefinition(name, includeAncestors);

            if (definition == null)
                continue;

            try
            {
                visitor.VisitObjectDefinition(definition);
            }
            catch (ObjectDefinitionStoreException ex)
            {
                throw new ObjectDefinitionStoreException(
                    definition.ResourceDescription, name, ex.Message);
            }
        }
    }

    private class TextProcessor
    {
        private readonly ILogger<TextProcessor> logger = LogManager.GetLogger<TextProcessor>();
        private readonly VariablePlaceholderConfigurer owner;
        private readonly IVariableSource variableSource;

        public TextProcessor(VariablePlaceholderConfigurer owner, IVariableSource variableSource)
        {
            this.owner = owner;
            this.variableSource = variableSource;
        }

        public string ParseAndResolveVariables(string rawStringValue)
        {
            return ParseAndResolveVariables(rawStringValue, new HashedSet());
        }

        private string ParseAndResolveVariables(string strVal, ISet visitedPlaceholders)
        {
            if (strVal == null)
            {
                return null;
            }

            int startIndex = strVal.IndexOf(owner.placeholderPrefix);
            while (startIndex != -1)
            {
                int endIndex = strVal.IndexOf(
                    owner.placeholderSuffix, startIndex + owner.placeholderPrefix.Length);
                if (endIndex != -1)
                {
                    int pos = startIndex + owner.placeholderPrefix.Length;
                    string placeholder = strVal.Substring(pos, endIndex - pos);
                    if (visitedPlaceholders.Contains(placeholder))
                    {
                        throw new ObjectDefinitionStoreException(
                            string.Format(
                                CultureInfo.InvariantCulture,
                                "Circular placeholder reference '{0}' detected. ",
                                placeholder));
                    }

                    visitedPlaceholders.Add(placeholder);

                    if (variableSource.CanResolveVariable(placeholder))
                    {
                        string resolvedValue = variableSource.ResolveVariable(placeholder);
                        resolvedValue = ParseAndResolveVariables(resolvedValue, visitedPlaceholders);

                        if (logger.IsEnabled(LogLevel.Debug))
                        {
                            logger.LogDebug(string.Format(
                                CultureInfo.InvariantCulture,
                                "Resolving placeholder '{0}' to '{1}'.", placeholder, resolvedValue));
                        }

                        if (resolvedValue == null
                            && startIndex == 0
                            && strVal.Length <= endIndex + owner.placeholderSuffix.Length)
                        {
                            return null;
                        }

                        strVal = strVal.Substring(0, startIndex) + resolvedValue + strVal.Substring(endIndex + owner.placeholderSuffix.Length);
                        startIndex = strVal.IndexOf(owner.placeholderPrefix, startIndex + (resolvedValue == null ? 0 : resolvedValue.Length));
                    }
                    else if (owner.ignoreUnresolvablePlaceholders)
                    {
                        // simply return the unprocessed value...
                        return strVal;
                    }
                    else
                    {
                        throw new ObjectDefinitionStoreException(string.Format(
                            CultureInfo.InvariantCulture,
                            "Could not resolve placeholder '{0}'.", placeholder));
                    }

                    visitedPlaceholders.Remove(placeholder);
                }
                else
                {
                    startIndex = -1;
                }
            }

            return strVal;
        }
    }

    private class CompositeVariableSource : IVariableSource
    {
        private readonly IList variableSourceList;

        public CompositeVariableSource(IList variableSourceList)
        {
            this.variableSourceList = variableSourceList;
        }

        public string ResolveVariable(string variableName)
        {
            foreach (IVariableSource variableSource in variableSourceList)
            {
                if (!variableSource.CanResolveVariable(variableName)) continue;

                return variableSource.ResolveVariable(variableName);
            }

            throw new ArgumentException(string.Format("cannot resolve variable '{0}'", variableName));
        }

        public bool CanResolveVariable(string variableName)
        {
            foreach (IVariableSource variableSource in variableSourceList)
            {
                if (variableSource.CanResolveVariable(variableName))
                    return true;
            }

            return false;
        }
    }
}
